use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::{IntoResponse, Json},
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use std::env;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

use service::{
    sea_orm::{Database, DatabaseConnection},
    Mutation as MutationCore, Query as QueryCore,
};

use entity::post::{self, Model};

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[tokio::main]
async fn start() -> anyhow::Result<()> {
    let app = app().await;
    let host = env::var("HOST").expect("HOST is not set in .env file");
    let port = env::var("PORT").expect("PORT is not set in .env file");
    let server_url = format!("{host}:{port}");
    let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap();
    axum::serve(listener, app).await?;

    Ok(())
}

#[allow(dead_code)]
async fn app() -> Router {
    env::set_var("RUST_LOG", "debug");
    tracing_subscriber::registry().with(fmt::layer()).init();

    let foo = 42;
    // 调用 `tracing` 包的 `info!`
    tracing::info!(foo, "Hello from tracing");

    dotenvy::dotenv().ok();

    let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");

    let conn = Database::connect(db_url)
        .await
        .expect("Database connection failed");

    let state = AppState { conn };
    Router::new()
        .route("/hello", get(|| async { "Hello, World!" }))
        .route("/", get(list_posts).post(create_post))
        .route("/:id", post(update_post))
        .route("/delete/:id", post(delete_post))
        .with_state(state)
}

#[derive(Clone)]
struct AppState {
    conn: DatabaseConnection,
}

#[derive(Serialize, Debug)]
struct SuccessResp {
    status: String,
    message: String,
}

#[derive(Serialize, Debug)]
struct PostsData {
    posts: Vec<Model>,
    num_pages: u64,
}

#[derive(Deserialize, Debug)]
struct Params {
    page: Option<u64>,
    posts_per_page: Option<u64>,
}

async fn list_posts(state: State<AppState>, Query(params): Query<Params>) -> impl IntoResponse {
    let page = params.page.unwrap_or(1);
    let posts_per_page = params.posts_per_page.unwrap_or(5);

    let (posts, num_pages) = QueryCore::find_posts_in_page(&state.conn, page, posts_per_page)
        .await
        .expect("Cannot find posts in page");

    (StatusCode::OK, Json(PostsData { posts, num_pages }))
}

async fn create_post(
    state: State<AppState>,
    axum::Json(info): axum::Json<post::Model>,
) -> impl IntoResponse {
    let create_result = MutationCore::create_post(&state.conn, info)
        .await
        .expect("could not insert post");

    (
        StatusCode::OK,
        Json(SuccessResp {
            status: "ok".into(),
            message: format!("create_result: {:?}", create_result),
        }),
    )
}

async fn update_post(
    state: State<AppState>,
    Path(id): Path<i32>,
    axum::Json(info): axum::Json<post::Model>,
) -> impl IntoResponse {
    let update_result = MutationCore::update_post_by_id(&state.conn, id, info)
        .await
        .expect("could not update post");

    (StatusCode::OK, Json(update_result))
}

async fn delete_post(state: State<AppState>, Path(id): Path<i32>) -> impl IntoResponse {
    MutationCore::delete_post(&state.conn, id)
        .await
        .expect("could not delete post");

    (StatusCode::OK, Json(id))
}

pub fn main() {
    let result = start();

    if let Some(err) = result.err() {
        println!("Error: {err}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        extract::connect_info::MockConnectInfo,
        http::{self, Request, StatusCode},
        Router,
    };
    use http_body_util::BodyExt; // for `collect`
    use tower::{Service, ServiceExt}; // for `call`, `oneshot`, and `ready`

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[tokio::test]
    async fn hello_world() {
        let app: Router = app().await;

        // `Router` implements `tower::Service<Request<Body>>` so we can
        // call it like any tower service, no need to run an HTTP server.
        let response = app
            .oneshot(
                Request::builder()
                    .uri("/hello")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);

        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"Hello, World!");
    }
}
